package ex3.visitors;

import common.exceptions.SemanticError;
import ic.ast.Visitor;
import ic.ast.decl.*;
import ic.ast.expr.*;
import ic.ast.stmt.*;
import ic.symbols.*;
import ic.types.MethodType;
import ic.types.TypeTable;

/**
 * Created with IntelliJ IDEA.
 * User: Roi
 * Date: 12/6/13
 *
 * A visitor responsible for checking the program's structure
 */
public class StructureCheckVisitor implements Visitor {

    boolean hasMain = false;

    // for checking that 'this' is used in the correct context.
    // the flag is turned on when entering a virtual method
    // and off when exiting it
    private boolean inVirtualMethodContext = false;

    // for checking the break and continue are used only inside loop context
    private boolean inLoopContext = false;

    @Override
    public Object visit(Program program) throws Exception {
        for (DeclClass icClass : program.getClasses()) {
            icClass.accept(this);
        }
        inLoopContext = false;
        if (!hasMain)
            structureError(program.getLine(), "No main method found");
        return null;
    }

    @Override
    public Object visit(DeclClass icClass) throws Exception {
        for (DeclField field : icClass.getFields()) {
            field.accept(this);
        }
        for (DeclMethod method : icClass.getMethods()) {
            MethodSymbol methodSymbol = (MethodSymbol) method.getEnclosingScope().lookup(method.getName());
            if (isMain(methodSymbol, method))
                hasMain = true;
            method.accept(this);
        }
        return null;
    }

    @Override
    public Object visit(DeclField field) {
        return null;
    }

    @Override
    public Object visit(DeclVirtualMethod method) throws Exception {
        inVirtualMethodContext = true;
        for (Statement stmt : method.getStatements()) {
            stmt.accept(this);
        }
        inVirtualMethodContext = false;
        return null;
    }

    @Override
    public Object visit(DeclStaticMethod method) throws Exception {
        for (Statement stmt : method.getStatements()) {
            stmt.accept(this);
        }
        return null;
    }

    @Override
    public Object visit(DeclLibraryMethod method) throws Exception {
        for (Statement stmt : method.getStatements()) {
            stmt.accept(this);
        }
        return null;
    }

    @Override
    public Object visit(Parameter formal) {
        return null;
    }

    @Override
    public Object visit(PrimitiveType type) {
        return null;
    }

    @Override
    public Object visit(ClassType type) {
        return null;
    }

    @Override
    public Object visit(StmtAssignment assignment) throws Exception {
        assignment.getAssignment().accept(this);
        assignment.getVariable().accept(this);
        return null;
    }

    @Override
    public Object visit(StmtCall callStatement) throws Exception {
        callStatement.getCall().accept(this);
        return null;
    }

    @Override
    public Object visit(StmtReturn returnStatement) throws Exception {
        if (returnStatement.getValue() == null) return null;
        returnStatement.getValue().accept(this);
        return null;
    }

    @Override
    public Object visit(StmtIf ifStatement) throws Exception {
        ifStatement.getCondition().accept(this);
        ifStatement.getOperation().accept(this);
        if (ifStatement.hasElse()) {
            ifStatement.getElseOperation().accept(this);
        }
        return null;
    }

    @Override
    public Object visit(StmtWhile whileStatement) throws Exception {
        whileStatement.getCondition().accept(this);
        if (!inLoopContext) {
            inLoopContext = true;
            whileStatement.getOperation().accept(this);
            inLoopContext = false;
        } else {
            whileStatement.getOperation().accept(this);
        }
        return null;
    }

    @Override
    public Object visit(StmtBreak breakStatement) throws Exception {
        if (!inLoopContext) {
            structureError(breakStatement.getLine(), "Use of 'break' statement outside of loop not allowed");
        }
        return null;
    }

    @Override
    public Object visit(StmtContinue continueStatement) throws Exception {
        if (!inLoopContext) {
            structureError(continueStatement.getLine(), "Use of 'continue' statement outside of loop not allowed");
        }
        return null;
    }

    @Override
    public Object visit(StmtBlock statementsBlock) throws Exception {
        for (Statement stmt : statementsBlock.getStatements()) {
            stmt.accept(this);
        }
        return null;
    }

    @Override
    public Object visit(LocalVariable localVariable) throws Exception {
        localVariable.getType().accept(this);
        if (localVariable.getInitialValue() == null) return null;
        localVariable.getInitialValue().accept(this);
        return null;
    }

    @Override
    public Object visit(RefVariable location) {
        return null;  // TODO
    }

    @Override
    public Object visit(RefField location) {
        return null;  // TODO
    }

    @Override
    public Object visit(RefArrayElement location) throws Exception {
        location.getArray().accept(this);
        location.getIndex().accept(this);
        return null;
    }

    @Override
    public Object visit(StaticCall call) throws Exception {
        for (Expression arg : call.getArguments()) {
            arg.accept(this);
        }
        return null;
    }

    @Override
    public Object visit(VirtualCall call) throws Exception {
        if (call.getObject() != null) {
            call.getObject().accept(this);
        }
        for (Expression arg : call.getArguments()) {
            arg.accept(this);
        }
        return null;
    }

    @Override
    public Object visit(This thisExpression) throws Exception {
        if (!inVirtualMethodContext) {
            structureError(thisExpression.getLine(), "Use of 'this' expression inside static method is not allowed");
        }
        return null;
    }

    @Override
    public Object visit(NewInstance newClass) {
        return null;
    }

    @Override
    public Object visit(NewArray newArray) throws Exception {
        newArray.getSize().accept(this);
        newArray.getType().accept(this);
        return null;
    }

    @Override
    public Object visit(Length length) throws Exception {
        length.getArray().accept(this);
        return null;
    }

    @Override
    public Object visit(BinaryOp binaryOp) throws Exception {
        binaryOp.getFirstOperand().accept(this);
        binaryOp.getSecondOperand().accept(this);
        return null;
    }

    @Override
    public Object visit(Literal literal) throws Exception {
        if (literal.getType() == PrimitiveType.DataType.INT) {
            try {
                Integer.parseInt((String) literal.getValue());
            } catch (NumberFormatException numberFormatErr) {
                structureError(literal.getLine(), "integer literal out of range");
            }
        }
        return null;
    }

    @Override
    public Object visit(UnaryOp unaryOp) throws Exception {
        Expression expr = unaryOp.getOperand();
        if (expr instanceof Literal) {
            Literal literal = (Literal) expr;
            if (literal.getType() == PrimitiveType.DataType.INT) {
                try {
                    long value = Long.parseLong((String) literal.getValue());
                    if (value > 2147483648L) { // check negative integer range
                        structureError(unaryOp.getLine(), "integer literal out of range");
                    }
                } catch (NumberFormatException numberFormatErr) {
                    structureError(unaryOp.getLine(), numberFormatErr.getMessage());
                }
            }
        } else {
            expr.accept(this);
        }

        return null;
    }

    @Override
    public Object visit(GlobalSymbolTable table) throws Exception {
        for (SymbolTable child : table.getSymbolTables()) {
            child.accept(this);
        }
        return null;
    }

    @Override
    public Object visit(ClassSymbolTable table) throws Exception {


        // Check fields override
        for (FieldSymbol field : table.getFieldSymbols()) {
            Symbol parentField = table.getParent().lookup(field.getID());
            if (parentField != null) {
                structureError(field.getLine(), "Field " + field.getID() + " is shadowing a field with the same name");
            }
        }

        // Check methods override
        for (MethodSymbol method : table.getMethodSymbols()) {
            Symbol parentField = table.getParent().lookup(method.getID());

            if (!(parentField == null)) {
                if (!parentField.getKind().equals(Kind.METHOD)) {
                    structureError(method.getLine(), "Method " + method.getID() + " is shadowing a field with the same name");
                }

                MethodType methodType = method.getMetType();
                MethodSymbol parentMethod = (MethodSymbol) parentField;
                MethodType parentType = parentMethod.getMetType();

                if (method.isStatic() && !parentMethod.isStatic()) {
                    structureError(method.getLine(), "Can't override " + method.getID() + " with a static method");
                }

                if (!method.isStatic() && parentMethod.isStatic()) {
                    structureError(method.getLine(), "Can't override " + method.getID() + " with an instance method");
                }

                if (!methodType.toString().equals(parentType.toString())) {
                    structureError(method.getLine(), "method '" + method.getID() + "' overloads a different method with the same name");
                }

            }
        }

        for (SymbolTable child : table.getSymbolTables()) {
            child.accept(this);
        }
        return null;
    }

    @Override
    public Object visit(BlockSymbolTable table) throws Exception {
        for (SymbolTable child : table.getSymbolTables()) {
            child.accept(this);
        }
        return null;
    }

    @Override
    public Object visit(MethodSymbolTable table) throws Exception {
        for (SymbolTable child : table.getSymbolTables()) {
            child.accept(this);
        }
        return null;
    }

    @Override
    public Object visit(SymbolTable symbolTable) throws Exception {
        for (SymbolTable child : symbolTable.getSymbolTables()) {
            child.accept(this);
        }
        return null;
    }

    private boolean isMain(MethodSymbol ms, DeclMethod m) throws Exception {
        if (!m.getName().equals("main")) {
            return false;
        }

        if (hasMain) {
            structureError(m.getLine(), "Found more than one main in the file");
        }

        if (!ms.isStatic()) {
            structureError(m.getLine(), "Main method must be static");
        }

        MethodType mt = ms.getMetType();
        if (!mt.getReturnType().getDisplayName().equals(TypeTable.getType("void").getDisplayName())) {
            structureError(m.getLine(), "Main method should have 'void' return type");
        }

        if (m.getFormals().size() != 1) {
            structureError(m.getLine(), "Argument for main method should be 'string[] args'");
        }

        Type ty = TypeTable.getType(ms.getMetType().getParamTypes().get(0), false);
        if ((!ty.getDisplayName().equals("string")) ||
                !(ty.getArrayDimension() == 1)) {
            structureError(m.getLine(), "Argument for main method should be 'string[] args'");
        }

        return true;
    }

    private void structureError(int line, String message) throws Exception {
        throw new SemanticError(line, message);
    }
}
